/*-------------------------------------------------------------------------
 *
 * verify_heapam.c
 *	  用于检查 PostgreSQL 堆关系损坏的函数
 *
 * Copyright (c) 2016-2022, PostgreSQL Global Development Group
 *
 *	  contrib/amcheck/verify_heapam.c
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/detoast.h"
#include "access/genam.h"
#include "access/heapam.h"
#include "access/heaptoast.h"
#include "access/multixact.h"
#include "access/toast_internals.h"
#include "access/visibilitymap.h"
#include "catalog/pg_am.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
#include "storage/procarray.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"

PG_FUNCTION_INFO_V1(verify_heapam);

/* 返回 verify_heapam 的元组中的列数 */
#define HEAPCHECK_RELATION_COLS 4

/* 最大有效 toast va_rawsize */
#define VARLENA_SIZE_LIMIT 0x3FFFFFFF

/*
 * 尽管名字如此，我们使用它来报告 XIDs 和
 * MXIDs 的问题。
 */
typedef enum XidBoundsViolation
{
	XID_INVALID,
	XID_IN_FUTURE,
	XID_PRECEDES_CLUSTERMIN,
	XID_PRECEDES_RELMIN,
	XID_BOUNDS_OK
} XidBoundsViolation;

typedef enum XidCommitStatus
{
	XID_COMMITTED,
	XID_IS_CURRENT_XID,
	XID_IN_PROGRESS,
	XID_ABORTED
} XidCommitStatus;

typedef enum SkipPages
{
	SKIP_PAGES_ALL_FROZEN,
	SKIP_PAGES_ALL_VISIBLE,
	SKIP_PAGES_NONE
} SkipPages;

/*
 * 结构体保存有关 toasted 属性的信息，足以检查
 * toasted 属性，并在发现其损坏时，报告在主表中
 * 遇到的位置。
 */
typedef struct ToastedAttribute
{
	struct varatt_external toast_pointer;
	BlockNumber blkno;			/* 主表中的块 */
	OffsetNumber offnum;		/* 主表中的偏移量 */
	AttrNumber	attnum;			/* 主表中的属性 */
} ToastedAttribute;

/*
 * 结构体保存 verify_heapam 执行生命周期中的
 * 运行上下文信息。
 */
typedef struct HeapCheckContext
{
	/*
	 * 来自 ShmemVariableCache 的值的缓存副本，以及
	 * 从它们计算得出的值。
	 */
	FullTransactionId next_fxid;	/* ShmemVariableCache->nextXid */
	TransactionId next_xid;		/* next_fxid 的 32 位版本 */
	TransactionId oldest_xid;	/* ShmemVariableCache->oldestXid */
	FullTransactionId oldest_fxid;	/* oldest_xid 的 64 位版本，相对于
									 * next_fxid 计算得出 */
	TransactionId safe_xmin;	/* 此 XID 和更新的 XID 在运行时无法变为
								 * 全部可见 */

	/*
	 * MultiXactState 的值的缓存副本
	 */
	MultiXactId next_mxact;		/* MultiXactState->nextMXact */
	MultiXactId oldest_mxact;	/* MultiXactState->oldestMultiXactId */

	/*
	 * 最近检查的 xid 及其状态的缓存副本。
	 */
	TransactionId cached_xid;
	XidCommitStatus cached_status;

	/* 关于正在检查的堆关系的值 */
	Relation	rel;
	TransactionId relfrozenxid;
	FullTransactionId relfrozenfxid;
	TransactionId relminmxid;
	Relation	toast_rel;
	Relation   *toast_indexes;
	Relation	valid_toast_index;
	int			num_toast_indexes;

	/* 遍历关系中的页面的值 */
	BlockNumber blkno;
	BufferAccessStrategy bstrategy;
	Buffer		buffer;
	Page		page;

	/* 遍历页面内元组的值 */
	OffsetNumber offnum;
	ItemId		itemid;
	uint16		lp_len;
	uint16		lp_off;
	HeapTupleHeader tuphdr;
	int			natts;

	/* 遍历元组内属性的值 */
	uint32		offset;			/* 在元组数据中的偏移量 */
	AttrNumber	attnum;

	/* 如果元组的 xmax 使其有资格进行修剪，则为真 */
	bool		tuple_could_be_pruned;

	/*
	 * 不可修剪且应检查的 toasted 属性的 ToastedAttribute 结构体列表
	 */
	List	   *toasted_attributes;

	/* verify_heapam 是否已经遇到任何损坏的元组 */
	bool		is_corrupt;

	/* verify_heapam 的结果元组的描述符和元组存储 */
	TupleDesc	tupdesc;
	Tuplestorestate *tupstore;
} HeapCheckContext;

/* 内部实现 */
static void fc_check_tuple(HeapCheckContext *fc_ctx);
static void fc_check_toast_tuple(HeapTuple fc_toasttup, HeapCheckContext *fc_ctx,
							  ToastedAttribute *fc_ta, int32 *fc_expected_chunk_seq,
							  uint32 fc_extsize);

static bool fc_check_tuple_attribute(HeapCheckContext *fc_ctx);
static void fc_check_toasted_attribute(HeapCheckContext *fc_ctx,
									ToastedAttribute *fc_ta);

static bool fc_check_tuple_header(HeapCheckContext *fc_ctx);
static bool fc_check_tuple_visibility(HeapCheckContext *fc_ctx);

static void fc_report_corruption(HeapCheckContext *fc_ctx, char *fc_msg);
static void fc_report_toast_corruption(HeapCheckContext *fc_ctx,
									ToastedAttribute *fc_ta, char *fc_msg);
static FullTransactionId fc_FullTransactionIdFromXidAndCtx(TransactionId fc_xid,
														const HeapCheckContext *fc_ctx);
static void fc_update_cached_xid_range(HeapCheckContext *fc_ctx);
static void fc_update_cached_mxid_range(HeapCheckContext *fc_ctx);
static XidBoundsViolation fc_check_mxid_in_range(MultiXactId fc_mxid,
											  HeapCheckContext *fc_ctx);
static XidBoundsViolation fc_check_mxid_valid_in_rel(MultiXactId fc_mxid,
												  HeapCheckContext *fc_ctx);
static XidBoundsViolation fc_get_xid_status(TransactionId fc_xid,
										 HeapCheckContext *fc_ctx,
										 XidCommitStatus *fc_status);

/*
 * 扫描并报告堆页面中的损坏，选择性地将吐槽属性与关联的吐槽表中的条目进行调和。旨在通过SQL调用，参数如下：
 *
 *   relation:
 *     要检查的堆关系的Oid。
 *
 *   on_error_stop:
 *     是否在检测到错误的第一页面结束时停止。请注意，可能返回多条记录。
 *
 *   check_toast:
 *     是否检查每个吐槽属性是否可以在吐槽表中找到。
 *
 *   skip:
 *     在堆关系中应跳过哪些类型的页面。有效选项为“all-visible”、“all-frozen”和“none”。
 *
 * 返回给SQL调用者一组元组，每个元组包含在堆中发现的损坏的位置和描述。
 *
 * 这段代码充分考虑到即使表页面严重损坏也不会导致服务器崩溃，但可能并不完美。如果check_toast为true，我们将使用常规索引查找尝试获取TOAST元组，如果吐槽表或索引中存在正确类型的损坏，确实可能会导致崩溃。无论您传递何种参数，我们都无法防止可能在尝试查找事务ID的提交状态时发生的崩溃（尽管我们避免尝试查找无法合法出现在表中的事务ID）。
 */
Datum verify_heapam(PG_FUNCTION_ARGS)
{
	ReturnSetInfo *fc_rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
	HeapCheckContext fc_ctx;
	Buffer		fc_vmbuffer = InvalidBuffer;
	Oid			fc_relid;
	bool		fc_on_error_stop;
	bool		fc_check_toast;
	SkipPages	fc_skip_option = SKIP_PAGES_NONE;
	BlockNumber fc_first_block;
	BlockNumber fc_last_block;
	BlockNumber fc_nblocks;
	const char *fc_skip;

	/* 检查提供的参数 */
	if (PG_ARGISNULL(0))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("relation cannot be null")));
	fc_relid = PG_GETARG_OID(0);

	if (PG_ARGISNULL(1))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("on_error_stop cannot be null")));
	fc_on_error_stop = PG_GETARG_BOOL(1);

	if (PG_ARGISNULL(2))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("check_toast cannot be null")));
	fc_check_toast = PG_GETARG_BOOL(2);

	if (PG_ARGISNULL(3))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("skip cannot be null")));
	fc_skip = text_to_cstring(PG_GETARG_TEXT_PP(3));
	if (pg_strcasecmp(fc_skip, "all-visible") == 0)
		fc_skip_option = SKIP_PAGES_ALL_VISIBLE;
	else if (pg_strcasecmp(fc_skip, "all-frozen") == 0)
		fc_skip_option = SKIP_PAGES_ALL_FROZEN;
	else if (pg_strcasecmp(fc_skip, "none") == 0)
		fc_skip_option = SKIP_PAGES_NONE;
	else
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("invalid skip option"),
				 errhint("Valid skip options are \"all-visible\", \"all-frozen\", and \"none\".")));

	memset(&fc_ctx, 0, sizeof(HeapCheckContext));
	fc_ctx.cached_xid = InvalidTransactionId;
	fc_ctx.toasted_attributes = NIL;

	/*
	 * 任何比我们的快照的xmin更新的xmin不能在我们运行时变得全可见。
	 */
	fc_ctx.safe_xmin = GetTransactionSnapshot()->xmin;

	/*
	 * 如果我们在未检查某个单独属性时报告损坏，则需要将attnum报告为NULL。在任何损坏报告可能发生之前设置该值。
	 */
	fc_ctx.attnum = -1;

	/* 构造元组存储和元组描述符 */
	InitMaterializedSRF(fcinfo, 0);
	fc_ctx.tupdesc = fc_rsinfo->setDesc;
	fc_ctx.tupstore = fc_rsinfo->setResult;

	/* 打开关系，检查relkind和访问方法 */
	fc_ctx.rel = relation_open(fc_relid, AccessShareLock);

	/*
	 * 检查一个关系的relkind和访问方法是否都受支持。
	 */
	if (!RELKIND_HAS_TABLE_AM(fc_ctx.rel->rd_rel->relkind) &&
		fc_ctx.rel->rd_rel->relkind != RELKIND_SEQUENCE)
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("cannot check relation \"%s\"",
						RelationGetRelationName(fc_ctx.rel)),
				 errdetail_relkind_not_supported(fc_ctx.rel->rd_rel->relkind)));

	/*
	 * 序列总是使用堆AM，但在目录中并不显示。其他relkind可能使用不同的AM，因此需要检查。
	 */
	if (fc_ctx.rel->rd_rel->relkind != RELKIND_SEQUENCE &&
		fc_ctx.rel->rd_rel->relam != HEAP_TABLE_AM_OID)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("only heap AM is supported")));

	/*
	 * 恢复期间对于未记录关系的早期退出。这些将没有关系分叉，因此不需要检查任何内容。我们将其视为关系为空。
	 */
	if (fc_ctx.rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED &&
		RecoveryInProgress())
	{
		ereport(DEBUG1,
				(errcode(ERRCODE_READ_ONLY_SQL_TRANSACTION),
				 errmsg("cannot verify unlogged relation \"%s\" during recovery, skipping",
						RelationGetRelationName(fc_ctx.rel))));
		relation_close(fc_ctx.rel, AccessShareLock);
		PG_RETURN_NULL();
	}

	/* 如果关系为空则早期退出 */
	fc_nblocks = RelationGetNumberOfBlocks(fc_ctx.rel);
	if (!fc_nblocks)
	{
		relation_close(fc_ctx.rel, AccessShareLock);
		PG_RETURN_NULL();
	}

	fc_ctx.bstrategy = GetAccessStrategy(BAS_BULKREAD);
	fc_ctx.buffer = InvalidBuffer;
	fc_ctx.page = NULL;

	/* 验证区块编号，或处理NULL。 */
	if (PG_ARGISNULL(4))
		fc_first_block = 0;
	else
	{
		int64		fc_fb = PG_GETARG_INT64(4);

		if (fc_fb < 0 || fc_fb >= fc_nblocks)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("starting block number must be between 0 and %u",
							fc_nblocks - 1)));
		fc_first_block = (BlockNumber) fc_fb;
	}
	if (PG_ARGISNULL(5))
		fc_last_block = fc_nblocks - 1;
	else
	{
		int64		fc_lb = PG_GETARG_INT64(5);

		if (fc_lb < 0 || fc_lb >= fc_nblocks)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("ending block number must be between 0 and %u",
							fc_nblocks - 1)));
		fc_last_block = (BlockNumber) fc_lb;
	}

	/* 选择性地打开吐槽关系（如果有）。 */
	if (fc_ctx.rel->rd_rel->reltoastrelid && fc_check_toast)
	{
		int			fc_offset;

		/* 主关系有关联的吐槽关系 */
		fc_ctx.toast_rel = table_open(fc_ctx.rel->rd_rel->reltoastrelid,
								   AccessShareLock);
		fc_offset = toast_open_indexes(fc_ctx.toast_rel,
									AccessShareLock,
									&(fc_ctx.toast_indexes),
									&(fc_ctx.num_toast_indexes));
		fc_ctx.valid_toast_index = fc_ctx.toast_indexes[fc_offset];
	}
	else
	{
		/*
		 * 主关系没有关联的吐槽关系，或我们
		 * 有意跳过它。
		 */
		fc_ctx.toast_rel = NULL;
		fc_ctx.toast_indexes = NULL;
		fc_ctx.num_toast_indexes = 0;
	}

	fc_update_cached_xid_range(&fc_ctx);
	fc_update_cached_mxid_range(&fc_ctx);
	fc_ctx.relfrozenxid = fc_ctx.rel->rd_rel->relfrozenxid;
	fc_ctx.relfrozenfxid = fc_FullTransactionIdFromXidAndCtx(fc_ctx.relfrozenxid, &fc_ctx);
	fc_ctx.relminmxid = fc_ctx.rel->rd_rel->relminmxid;

	if (TransactionIdIsNormal(fc_ctx.relfrozenxid))
		fc_ctx.oldest_xid = fc_ctx.relfrozenxid;

	for (fc_ctx.blkno = fc_first_block; fc_ctx.blkno <= fc_last_block; fc_ctx.blkno++)
	{
		OffsetNumber fc_maxoff;

		CHECK_FOR_INTERRUPTS();

		/* 选择性地跳过所有冻结或全可见的区块 */
		if (fc_skip_option != SKIP_PAGES_NONE)
		{
			int32		fc_mapbits;

			fc_mapbits = (int32) visibilitymap_get_status(fc_ctx.rel, fc_ctx.blkno,
													   &fc_vmbuffer);
			if (fc_skip_option == SKIP_PAGES_ALL_FROZEN)
			{
				if ((fc_mapbits & VISIBILITYMAP_ALL_FROZEN) != 0)
					continue;
			}

			if (fc_skip_option == SKIP_PAGES_ALL_VISIBLE)
			{
				if ((fc_mapbits & VISIBILITYMAP_ALL_VISIBLE) != 0)
					continue;
			}
		}

		/* 读取并锁定下一页。 */
		fc_ctx.buffer = ReadBufferExtended(fc_ctx.rel, MAIN_FORKNUM, fc_ctx.blkno,
										RBM_NORMAL, fc_ctx.bstrategy);
		LockBuffer(fc_ctx.buffer, BUFFER_LOCK_SHARE);
		fc_ctx.page = BufferGetPage(fc_ctx.buffer);

		/* 执行元组检查 */
		fc_maxoff = PageGetMaxOffsetNumber(fc_ctx.page);
		for (fc_ctx.offnum = FirstOffsetNumber; fc_ctx.offnum <= fc_maxoff;
			 fc_ctx.offnum = OffsetNumberNext(fc_ctx.offnum))
		{
			fc_ctx.itemid = PageGetItemId(fc_ctx.page, fc_ctx.offnum);

			/* 跳过未使用/已死的行指针 */
			if (!ItemIdIsUsed(fc_ctx.itemid) || ItemIdIsDead(fc_ctx.itemid))
				continue;

			/*
			 * 如果该行指针已被重定向，请检查它
			 * 是否重定向到行指针数组中的有效偏移量
			 */
			if (ItemIdIsRedirected(fc_ctx.itemid))
			{
				OffsetNumber fc_rdoffnum = ItemIdGetRedirect(fc_ctx.itemid);
				ItemId		fc_rditem;

				if (fc_rdoffnum < FirstOffsetNumber)
				{
					fc_report_corruption(&fc_ctx,
									  psprintf("line pointer redirection to item at offset %u precedes minimum offset %u",
											   (unsigned) fc_rdoffnum,
											   (unsigned) FirstOffsetNumber));
					continue;
				}
				if (fc_rdoffnum > fc_maxoff)
				{
					fc_report_corruption(&fc_ctx,
									  psprintf("line pointer redirection to item at offset %u exceeds maximum offset %u",
											   (unsigned) fc_rdoffnum,
											   (unsigned) fc_maxoff));
					continue;
				}
				fc_rditem = PageGetItemId(fc_ctx.page, fc_rdoffnum);
				if (!ItemIdIsUsed(fc_rditem))
					fc_report_corruption(&fc_ctx,
									  psprintf("line pointer redirection to unused item at offset %u",
											   (unsigned) fc_rdoffnum));
				continue;
			}

			/* sanity-check行指针的偏移和长度值 */
			fc_ctx.lp_len = ItemIdGetLength(fc_ctx.itemid);
			fc_ctx.lp_off = ItemIdGetOffset(fc_ctx.itemid);

			if (fc_ctx.lp_off != MAXALIGN(fc_ctx.lp_off))
			{
				fc_report_corruption(&fc_ctx,
								  psprintf("line pointer to page offset %u is not maximally aligned",
										   fc_ctx.lp_off));
				continue;
			}
			if (fc_ctx.lp_len < MAXALIGN(SizeofHeapTupleHeader))
			{
				fc_report_corruption(&fc_ctx,
								  psprintf("line pointer length %u is less than the minimum tuple header size %u",
										   fc_ctx.lp_len,
										   (unsigned) MAXALIGN(SizeofHeapTupleHeader)));
				continue;
			}
			if (fc_ctx.lp_off + fc_ctx.lp_len > BLCKSZ)
			{
				fc_report_corruption(&fc_ctx,
								  psprintf("line pointer to page offset %u with length %u ends beyond maximum page offset %u",
										   fc_ctx.lp_off,
										   fc_ctx.lp_len,
										   (unsigned) BLCKSZ));
				continue;
			}

			/* 至少检查元组的头部应该是安全的 */
			fc_ctx.tuphdr = (HeapTupleHeader) PageGetItem(fc_ctx.page, fc_ctx.itemid);
			fc_ctx.natts = HeapTupleHeaderGetNatts(fc_ctx.tuphdr);

			/* 好的，准备检查下一个元组 */
			fc_check_tuple(&fc_ctx);
		}

		/* 清理 */
		UnlockReleaseBuffer(fc_ctx.buffer);

		/*
		 * 检查我们刚刚释放的页面上的任何 toast 指针
		 */
		if (fc_ctx.toasted_attributes != NIL)
		{
			ListCell   *fc_cell;

			foreach(fc_cell, fc_ctx.toasted_attributes)
				fc_check_toasted_attribute(&fc_ctx, lfirst(fc_cell));
			list_free_deep(fc_ctx.toasted_attributes);
			fc_ctx.toasted_attributes = NIL;
		}

		if (fc_on_error_stop && fc_ctx.is_corrupt)
			break;
	}

	if (fc_vmbuffer != InvalidBuffer)
		ReleaseBuffer(fc_vmbuffer);

	/* 关闭相关的 toast 表和索引（如果有的话）。 */
	if (fc_ctx.toast_indexes)
		toast_close_indexes(fc_ctx.toast_indexes, fc_ctx.num_toast_indexes,
							AccessShareLock);
	if (fc_ctx.toast_rel)
		table_close(fc_ctx.toast_rel, AccessShareLock);

	/* 关闭主关系 */
	relation_close(fc_ctx.rel, AccessShareLock);

	PG_RETURN_NULL();
}

/*
 * report_corruption 和 report_toast_corruption 的共享内部实现。
 */
static void fc_report_corruption_internal(Tuplestorestate *fc_tupstore, TupleDesc fc_tupdesc,
						   BlockNumber fc_blkno, OffsetNumber fc_offnum,
						   AttrNumber fc_attnum, char *fc_msg)
{
	Datum		fc_values[HEAPCHECK_RELATION_COLS];
	bool		fc_nulls[HEAPCHECK_RELATION_COLS];
	HeapTuple	fc_tuple;

	MemSet(fc_values, 0, sizeof(fc_values));
	MemSet(fc_nulls, 0, sizeof(fc_nulls));
	fc_values[0] = Int64GetDatum(fc_blkno);
	fc_values[1] = Int32GetDatum(fc_offnum);
	fc_values[2] = Int32GetDatum(fc_attnum);
	fc_nulls[2] = (fc_attnum < 0);
	fc_values[3] = CStringGetTextDatum(fc_msg);

	/*
	 * 原则上，没有什么可以阻止对一个大型、高度损坏的表进行扫描，
	 * 在构建 tuplestore 时使用的工作内存的确可以积累起来。没关系，
	 * 但如果我们也将 msg 参数的内存泄漏到查询结束，我们可能会超过
	 * 工作内存的边界，超过一个微不足道的量。因此，每次被调用时
	 * 释放 msg 参数，而不是等到当前内存上下文被释放。
	 */
	pfree(fc_msg);

	fc_tuple = heap_form_tuple(fc_tupdesc, fc_values, fc_nulls);
	tuplestore_puttuple(fc_tupstore, fc_tuple);
}

/*
 * 记录在主表中发现的单个损坏。ctx 中的值应该指示损坏的位置，
 * msg 参数应该包含一个人类可读的损坏描述。
 *
 * msg 参数由该函数 pfree。
 */
static void fc_report_corruption(HeapCheckContext *fc_ctx, char *fc_msg)
{
	fc_report_corruption_internal(fc_ctx->tupstore, fc_ctx->tupdesc, fc_ctx->blkno,
							   fc_ctx->offnum, fc_ctx->attnum, fc_msg);
	fc_ctx->is_corrupt = true;
}

/*
 * 记录在 toast 表中发现的损坏。ta 中的值应该指示在主表中
 * 遇到 toast 指针的位置，msg 参数应该包含一个人类可读的
 * toast 表损坏描述。
 *
 * 与上面相同，msg 参数由该函数 pfree。
 */
static void fc_report_toast_corruption(HeapCheckContext *fc_ctx, ToastedAttribute *fc_ta,
						char *fc_msg)
{
	fc_report_corruption_internal(fc_ctx->tupstore, fc_ctx->tupdesc, fc_ta->blkno,
							   fc_ta->offnum, fc_ta->attnum, fc_msg);
	fc_ctx->is_corrupt = true;
}

/*
 * 检查元组头的损坏。
 *
 * 某些类型的损坏使得检查元组属性变得不安全，例如当行指针
 * 指向超出页面的字节范围。在这种情况下，我们记录适当的损坏消息后返回 false（不可检查）。
 *
 * 其他一些类型的元组头损坏会混淆元组属性开始的位置
 * 或 null 位图的长度等问题，使得即使所有候选答案
 * 都不会导致读取超出行指针或页面的末尾，尝试检查属性
 * 也是不合理的。在这种情况下，像上面一样，我们记录
 * 头部的损坏消息，然后返回 false。
 *
 * 其他类型的元组头损坏不影响元组属性是否可以检查的问题，
 * 所以我们记录这些损坏消息，但仅仅因为我们检测到了它们
 * 而不返回 false。
 *
 * 返回这个元组是否足够合理以进行可见性和属性检查。
 */
static bool fc_check_tuple_header(HeapCheckContext *fc_ctx)
{
	HeapTupleHeader fc_tuphdr = fc_ctx->tuphdr;
	uint16		fc_infomask = fc_tuphdr->t_infomask;
	bool		fc_result = true;
	unsigned	fc_expected_hoff;

	if (fc_ctx->tuphdr->t_hoff > fc_ctx->lp_len)
	{
		fc_report_corruption(fc_ctx,
						  psprintf("data begins at offset %u beyond the tuple length %u",
								   fc_ctx->tuphdr->t_hoff, fc_ctx->lp_len));
		fc_result = false;
	}

	if ((fc_ctx->tuphdr->t_infomask & HEAP_XMAX_COMMITTED) &&
		(fc_ctx->tuphdr->t_infomask & HEAP_XMAX_IS_MULTI))
	{
		fc_report_corruption(fc_ctx,
						  pstrdup("multixact should not be marked committed"));

		/*
		 * 这个条件显然是错误的，但这不足以证明
		 * 跳过进一步检查，因为我们不依靠这个来确定
		 * 元组是否可见或解释其他相关的头字段。
		 */
	}

	if (fc_infomask & HEAP_HASNULL)
		fc_expected_hoff = MAXALIGN(SizeofHeapTupleHeader + BITMAPLEN(fc_ctx->natts));
	else
		fc_expected_hoff = MAXALIGN(SizeofHeapTupleHeader);
	if (fc_ctx->tuphdr->t_hoff != fc_expected_hoff)
	{
		if ((fc_infomask & HEAP_HASNULL) && fc_ctx->natts == 1)
			fc_report_corruption(fc_ctx,
							  psprintf("tuple data should begin at byte %u, but actually begins at byte %u (1 attribute, has nulls)",
									   fc_expected_hoff, fc_ctx->tuphdr->t_hoff));
		else if ((fc_infomask & HEAP_HASNULL))
			fc_report_corruption(fc_ctx,
							  psprintf("tuple data should begin at byte %u, but actually begins at byte %u (%u attributes, has nulls)",
									   fc_expected_hoff, fc_ctx->tuphdr->t_hoff, fc_ctx->natts));
		else if (fc_ctx->natts == 1)
			fc_report_corruption(fc_ctx,
							  psprintf("tuple data should begin at byte %u, but actually begins at byte %u (1 attribute, no nulls)",
									   fc_expected_hoff, fc_ctx->tuphdr->t_hoff));
		else
			fc_report_corruption(fc_ctx,
							  psprintf("tuple data should begin at byte %u, but actually begins at byte %u (%u attributes, no nulls)",
									   fc_expected_hoff, fc_ctx->tuphdr->t_hoff, fc_ctx->natts));
		fc_result = false;
	}

	return fc_result;
}

/*
 * 检查元组的可见性，以便我们知道哪些进一步的检查是安全的
 * 执行。
 *
 * 如果一个元组可能是由一个同时向表中添加了
 * 列的事务插入的，但最终没有提交，或者尚未
 * 提交，那么表的当前 TupleDesc 可能与构造该元组时使用的
 * 不同，因此我们必须不检查它。
 *
 * 作为一种特殊情况，如果我们自己的事务插入了该元组，即使我们
 * 向表中添加了列，我们的 TupleDesc 应该匹配。我们可以检查
 * 该元组，但选择不这样做。
 *
 * 如果一个元组已经被更新或删除，我们仍然可以读取旧元组以进行
 * 损坏检查，只要我们小心并发的 vacuum。主表元组本身不能
 * 被 vacuum 删除，因为我们持有该页面的缓冲锁，但如果
 * 删除事务的时间戳早于我们的事务快照的 xmin，那么 vacuum
 * 可能会在任何时候删除 toast，因此我们必须不尝试跟踪 TOAST 指针。
 *
 * 如果 xmin 或 xmax 值早于可以与 clog 检查的时间，或似乎
 * 在未来（可能由于循环），那么我们无法确定元组的可见性，
 * 所以我们跳过进一步检查。
 *
 * 如果元组本身应该被检查，则返回 true，否则返回 false。设置
 * ctx->tuple_could_be_pruned，如果该元组——以及任何相关的
 * TOAST 元组——有资格进行修剪。
 */
static bool fc_check_tuple_visibility(HeapCheckContext *fc_ctx)
{
	TransactionId fc_xmin;
	TransactionId fc_xvac;
	TransactionId fc_xmax;
	XidCommitStatus fc_xmin_status;
	XidCommitStatus fc_xvac_status;
	XidCommitStatus fc_xmax_status;
	HeapTupleHeader fc_tuphdr = fc_ctx->tuphdr;

	fc_ctx->tuple_could_be_pruned = true;	/* 尚未证明其他情况 */

	/* 如果 xmin 是正常的，它应该在有效范围内 */
	fc_xmin = HeapTupleHeaderGetXmin(fc_tuphdr);
	switch (fc_get_xid_status(fc_xmin, fc_ctx, &fc_xmin_status))
	{
		case XID_INVALID:
			/* 可能是中止的投机性插入的结果。 */
			return false;
		case XID_BOUNDS_OK:
			break;
		case XID_IN_FUTURE:
			fc_report_corruption(fc_ctx,
							  psprintf("xmin %u equals or exceeds next valid transaction ID %u:%u",
									   fc_xmin,
									   EpochFromFullTransactionId(fc_ctx->next_fxid),
									   XidFromFullTransactionId(fc_ctx->next_fxid)));
			return false;
		case XID_PRECEDES_CLUSTERMIN:
			fc_report_corruption(fc_ctx,
							  psprintf("xmin %u precedes oldest valid transaction ID %u:%u",
									   fc_xmin,
									   EpochFromFullTransactionId(fc_ctx->oldest_fxid),
									   XidFromFullTransactionId(fc_ctx->oldest_fxid)));
			return false;
		case XID_PRECEDES_RELMIN:
			fc_report_corruption(fc_ctx,
							  psprintf("xmin %u precedes relation freeze threshold %u:%u",
									   fc_xmin,
									   EpochFromFullTransactionId(fc_ctx->relfrozenfxid),
									   XidFromFullTransactionId(fc_ctx->relfrozenfxid)));
			return false;
	}

	/*
	 * 插入事务已经提交了吗？
	 */
	if (!HeapTupleHeaderXminCommitted(fc_tuphdr))
	{
		if (HeapTupleHeaderXminInvalid(fc_tuphdr))
			return false;		/* 插入者中止，不检查 */
		/* 由 9.0 之前的二进制升级使用 */
		else if (fc_tuphdr->t_infomask & HEAP_MOVED_OFF)
		{
			fc_xvac = HeapTupleHeaderGetXvac(fc_tuphdr);

			switch (fc_get_xid_status(fc_xvac, fc_ctx, &fc_xvac_status))
			{
				case XID_INVALID:
					fc_report_corruption(fc_ctx,
									  pstrdup("old-style VACUUM FULL transaction ID for moved off tuple is invalid"));
					return false;
				case XID_IN_FUTURE:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved off tuple equals or exceeds next valid transaction ID %u:%u",
											   fc_xvac,
											   EpochFromFullTransactionId(fc_ctx->next_fxid),
											   XidFromFullTransactionId(fc_ctx->next_fxid)));
					return false;
				case XID_PRECEDES_RELMIN:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved off tuple precedes relation freeze threshold %u:%u",
											   fc_xvac,
											   EpochFromFullTransactionId(fc_ctx->relfrozenfxid),
											   XidFromFullTransactionId(fc_ctx->relfrozenfxid)));
					return false;
				case XID_PRECEDES_CLUSTERMIN:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved off tuple precedes oldest valid transaction ID %u:%u",
											   fc_xvac,
											   EpochFromFullTransactionId(fc_ctx->oldest_fxid),
											   XidFromFullTransactionId(fc_ctx->oldest_fxid)));
					return false;
				case XID_BOUNDS_OK:
					break;
			}

			switch (fc_xvac_status)
			{
				case XID_IS_CURRENT_XID:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved off tuple matches our current transaction ID",
											   fc_xvac));
					return false;
				case XID_IN_PROGRESS:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved off tuple appears to be in progress",
											   fc_xvac));
					return false;

				case XID_COMMITTED:

					/*
					 * 该元组是死的，因为 xvac 事务将其移除
					 * 并提交了。它是可检查的，但也
					 * 可以被修剪。
					 */
					return true;

				case XID_ABORTED:

					/*
					 * 原始的 xmin 必须已经提交，因为 xvac
					 * 事务试图将其移除。由于 xvac 已被
					 * 中止，它现在是否仍然存活取决于
					 * xmax 的状态。
					 */
					break;
			}
		}
		/* 由 9.0 之前的二进制升级使用 */
		else if (fc_tuphdr->t_infomask & HEAP_MOVED_IN)
		{
			fc_xvac = HeapTupleHeaderGetXvac(fc_tuphdr);

			switch (fc_get_xid_status(fc_xvac, fc_ctx, &fc_xvac_status))
			{
				case XID_INVALID:
					fc_report_corruption(fc_ctx,
									  pstrdup("old-style VACUUM FULL transaction ID for moved in tuple is invalid"));
					return false;
				case XID_IN_FUTURE:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved in tuple equals or exceeds next valid transaction ID %u:%u",
											   fc_xvac,
											   EpochFromFullTransactionId(fc_ctx->next_fxid),
											   XidFromFullTransactionId(fc_ctx->next_fxid)));
					return false;
				case XID_PRECEDES_RELMIN:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved in tuple precedes relation freeze threshold %u:%u",
											   fc_xvac,
											   EpochFromFullTransactionId(fc_ctx->relfrozenfxid),
											   XidFromFullTransactionId(fc_ctx->relfrozenfxid)));
					return false;
				case XID_PRECEDES_CLUSTERMIN:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved in tuple precedes oldest valid transaction ID %u:%u",
											   fc_xvac,
											   EpochFromFullTransactionId(fc_ctx->oldest_fxid),
											   XidFromFullTransactionId(fc_ctx->oldest_fxid)));
					return false;
				case XID_BOUNDS_OK:
					break;
			}

			switch (fc_xvac_status)
			{
				case XID_IS_CURRENT_XID:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved in tuple matches our current transaction ID",
											   fc_xvac));
					return false;
				case XID_IN_PROGRESS:
					fc_report_corruption(fc_ctx,
									  psprintf("old-style VACUUM FULL transaction ID %u for moved in tuple appears to be in progress",
											   fc_xvac));
					return false;

				case XID_COMMITTED:

					/*
					 * 原始的 xmin 必须已经提交，因为 xvac
					 * 事务在之后将其移除。它现在是否仍然存活
					 * 取决于 xmax 的状态。
					 */
					break;

				case XID_ABORTED:

					/*
					 * 该元组是死的，因为 xvac 事务将其移除
					 * 并提交了。它是可检查的，但也
					 * 可以被修剪。
					 */
					return true;
			}
		}
		else if (fc_xmin_status != XID_COMMITTED)
		{
			/*
			 * 插入事务并未进行，且尚未提交，因此
			 * 可能已以我们不知道的方式更改了TupleDesc。
			 * 因此，不要尝试检查元组结构。
			 *
			 * 如果xmin_status恰好是XID_IS_CURRENT_XID，那么理论上
			 * 任何此类DDL更改都应该对我们可见，因此在这种情况下，
			 * 也许我们可以检查一下。但目前，让我们采取
			 * 保守态度，将其视为任何其他未提交的插入。
			 */
			return false;
		}
	}

	/*
	 * 好吧，插入者已经提交，因此在某个时候是好的。现在，删除事务怎么样？
	 */

	if (fc_tuphdr->t_infomask & HEAP_XMAX_IS_MULTI)
	{
		/*
		 * xmax是一个多重事务，所以要对MXID进行合理性检查。请注意，我们在检查
		 * HEAP_XMAX_INVALID或
		 * HEAP_XMAX_IS_LOCKED_ONLY之前进行此检查。因此，这可能会抱怨
		 * 在正常扫描期间实际上不会有问题的事情，
		 * 但最终我们将不得不冻结，而该过程将
		 * 忽略提示位。
		 *
		 * 即使MXID超出范围，我们仍然知道原始
		 * 插入已提交，因此我们可以检查元组本身。然而，我们
		 * 不能排除这个元组已死的可能性，因此不要清除
		 * ctx->tuple_could_be_pruned。可能我们应该在
		 * HEAP_XMAX_INVALID被设置或
		 * HEAP_XMAX_IS_LOCKED_ONLY为真时继续清除该标志，但目前我们倾向于
		 * 避免可能虚假的缺失TOAST条目的投诉。
		 */
		fc_xmax = HeapTupleHeaderGetRawXmax(fc_tuphdr);
		switch (fc_check_mxid_valid_in_rel(fc_xmax, fc_ctx))
		{
			case XID_INVALID:
				fc_report_corruption(fc_ctx,
								  pstrdup("multitransaction ID is invalid"));
				return true;
			case XID_PRECEDES_RELMIN:
				fc_report_corruption(fc_ctx,
								  psprintf("multitransaction ID %u precedes relation minimum multitransaction ID threshold %u",
										   fc_xmax, fc_ctx->relminmxid));
				return true;
			case XID_PRECEDES_CLUSTERMIN:
				fc_report_corruption(fc_ctx,
								  psprintf("multitransaction ID %u precedes oldest valid multitransaction ID threshold %u",
										   fc_xmax, fc_ctx->oldest_mxact));
				return true;
			case XID_IN_FUTURE:
				fc_report_corruption(fc_ctx,
								  psprintf("multitransaction ID %u equals or exceeds next valid multitransaction ID %u",
										   fc_xmax,
										   fc_ctx->next_mxact));
				return true;
			case XID_BOUNDS_OK:
				break;
		}
	}

	if (fc_tuphdr->t_infomask & HEAP_XMAX_INVALID)
	{
		/*
		 * 这个元组是活的。一个并发运行的事务可以
		 * 在我们检查TOAST之前删除它，但任何此类
		 * 运行中的事务肯定不小于我们的safe_xmin，因此该
		 * TOAST不能在我们之下被清除。
		 */
		fc_ctx->tuple_could_be_pruned = false;
		return true;
	}

	if (HEAP_XMAX_IS_LOCKED_ONLY(fc_tuphdr->t_infomask))
	{
		/*
		 * “删除”事务实际上只对其进行了锁定，因此无论如何
		 * 元组是活的。如上所述，一个并发运行的事务可能会删除
		 * 它，但它不能在我们之下被清除。
		 */
		fc_ctx->tuple_could_be_pruned = false;
		return true;
	}

	if (fc_tuphdr->t_infomask & HEAP_XMAX_IS_MULTI)
	{
		/*
		 * 我们已经检查过这个多重事务对于
		 * 该表在限制之内。现在检查这个多重事务的更新xid。
		 */
		fc_xmax = HeapTupleGetUpdateXid(fc_tuphdr);
		switch (fc_get_xid_status(fc_xmax, fc_ctx, &fc_xmax_status))
		{
			case XID_INVALID:
				/* 不是 LOCKED_ONLY，因此必须有一个 xmax */
				fc_report_corruption(fc_ctx,
								  pstrdup("update xid is invalid"));
				return true;
			case XID_IN_FUTURE:
				fc_report_corruption(fc_ctx,
								  psprintf("update xid %u equals or exceeds next valid transaction ID %u:%u",
										   fc_xmax,
										   EpochFromFullTransactionId(fc_ctx->next_fxid),
										   XidFromFullTransactionId(fc_ctx->next_fxid)));
				return true;
			case XID_PRECEDES_RELMIN:
				fc_report_corruption(fc_ctx,
								  psprintf("update xid %u precedes relation freeze threshold %u:%u",
										   fc_xmax,
										   EpochFromFullTransactionId(fc_ctx->relfrozenfxid),
										   XidFromFullTransactionId(fc_ctx->relfrozenfxid)));
				return true;
			case XID_PRECEDES_CLUSTERMIN:
				fc_report_corruption(fc_ctx,
								  psprintf("update xid %u precedes oldest valid transaction ID %u:%u",
										   fc_xmax,
										   EpochFromFullTransactionId(fc_ctx->oldest_fxid),
										   XidFromFullTransactionId(fc_ctx->oldest_fxid)));
				return true;
			case XID_BOUNDS_OK:
				break;
		}

		switch (fc_xmax_status)
		{
			case XID_IS_CURRENT_XID:
			case XID_IN_PROGRESS:

				/*
				 * 删除正在进行中，因此它不能对我们的
				 * 快照可见。
				 */
				fc_ctx->tuple_could_be_pruned = false;
				break;
			case XID_COMMITTED:

				/*
				 * 删除已提交。TOAST是否可以被清除
				 * 取决于删除事务的年龄。
				 */
				fc_ctx->tuple_could_be_pruned = TransactionIdPrecedes(fc_xmax,
																   fc_ctx->safe_xmin);
				break;
			case XID_ABORTED:

				/*
				 * 删除已中止或崩溃。元组仍然是活的。
				 */
				fc_ctx->tuple_could_be_pruned = false;
				break;
		}

		/* 即使元组已死，其本身也是可以检查的。 */
		return true;
	}

	/* xmax是一个XID，而不是MXID。合理性检查它。 */
	fc_xmax = HeapTupleHeaderGetRawXmax(fc_tuphdr);
	switch (fc_get_xid_status(fc_xmax, fc_ctx, &fc_xmax_status))
	{
		case XID_INVALID:
			fc_ctx->tuple_could_be_pruned = false;
			return true;
		case XID_IN_FUTURE:
			fc_report_corruption(fc_ctx,
							  psprintf("xmax %u equals or exceeds next valid transaction ID %u:%u",
									   fc_xmax,
									   EpochFromFullTransactionId(fc_ctx->next_fxid),
									   XidFromFullTransactionId(fc_ctx->next_fxid)));
			return false;		/* 损坏 */
		case XID_PRECEDES_RELMIN:
			fc_report_corruption(fc_ctx,
							  psprintf("xmax %u precedes relation freeze threshold %u:%u",
									   fc_xmax,
									   EpochFromFullTransactionId(fc_ctx->relfrozenfxid),
									   XidFromFullTransactionId(fc_ctx->relfrozenfxid)));
			return false;		/* 损坏 */
		case XID_PRECEDES_CLUSTERMIN:
			fc_report_corruption(fc_ctx,
							  psprintf("xmax %u precedes oldest valid transaction ID %u:%u",
									   fc_xmax,
									   EpochFromFullTransactionId(fc_ctx->oldest_fxid),
									   XidFromFullTransactionId(fc_ctx->oldest_fxid)));
			return false;		/* 损坏 */
		case XID_BOUNDS_OK:
			break;
	}

	/*
	 * TOAST是否可以被清除取决于删除
	 * 事务的年龄。
	 */
	switch (fc_xmax_status)
	{
		case XID_IS_CURRENT_XID:
		case XID_IN_PROGRESS:

			/*
			 * 删除正在进行中，因此它不能对我们的
			 * 快照可见。
			 */
			fc_ctx->tuple_could_be_pruned = false;
			break;

		case XID_COMMITTED:

			/*
			 * 删除已提交。TOAST是否可以被清除
			 * 取决于删除事务的年龄。
			 */
			fc_ctx->tuple_could_be_pruned = TransactionIdPrecedes(fc_xmax,
															   fc_ctx->safe_xmin);
			break;

		case XID_ABORTED:

			/*
			 * 删除操作被中止或崩溃。元组仍然是活动的。
			 */
			fc_ctx->tuple_could_be_pruned = false;
			break;
	}

	/* 即使元组已死，其本身也是可以检查的。 */
	return true;
}


/*
 * 检查当前的 toast 元组与 ctx 中跟踪的状态，记录
 * 在 ctx->tupstore 中发现的任何损坏。
 *
 * 这并不等同于在 toast 表本身上运行 verify_heapam，
 * 并且不针对 toast 表的损坏进行加固。相反，在
 * 验证主表中的一个刚好属性时，检索存储
 * 刚好值的 toast 元组序列并按顺序检查，
 * 每个 toast 元组都与我们在序列中的位置进行比较，
 * 以及每个 toast 元组的 varlena 结构进行健全性检查。
 *
 * 进入时，*expected_chunk_seq 应该是我们期望
 * 在 toasttup 中找到的 chunk_seq 值。退出时，它将更新为
 * 下一次调用此函数时应期望的值。
 */
static void fc_check_toast_tuple(HeapTuple fc_toasttup, HeapCheckContext *fc_ctx,
				  ToastedAttribute *fc_ta, int32 *fc_expected_chunk_seq,
				  uint32 fc_extsize)
{
	int32		fc_chunk_seq;
	int32		fc_last_chunk_seq = (fc_extsize - 1) / TOAST_MAX_CHUNK_SIZE;
	Pointer		fc_chunk;
	bool		fc_isnull;
	int32		fc_chunksize;
	int32		fc_expected_size;

	/* 健全性检查序列号。 */
	fc_chunk_seq = DatumGetInt32(fastgetattr(fc_toasttup, 2,
										  fc_ctx->toast_rel->rd_att, &fc_isnull));
	if (fc_isnull)
	{
		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u has toast chunk with null sequence number",
										 fc_ta->toast_pointer.va_valueid));
		return;
	}
	if (fc_chunk_seq != *fc_expected_chunk_seq)
	{
		/* 要么 TOAST 索引损坏，要么我们没有所有的块。 */
		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u index scan returned chunk %d when expecting chunk %d",
										 fc_ta->toast_pointer.va_valueid,
										 fc_chunk_seq, *fc_expected_chunk_seq));
	}
	*fc_expected_chunk_seq = fc_chunk_seq + 1;

	/* 健全性检查块数据。 */
	fc_chunk = DatumGetPointer(fastgetattr(fc_toasttup, 3,
										fc_ctx->toast_rel->rd_att, &fc_isnull));
	if (fc_isnull)
	{
		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u chunk %d has null data",
										 fc_ta->toast_pointer.va_valueid,
										 fc_chunk_seq));
		return;
	}
	if (!VARATT_IS_EXTENDED(fc_chunk))
		fc_chunksize = VARSIZE(fc_chunk) - VARHDRSZ;
	else if (VARATT_IS_SHORT(fc_chunk))
	{
		/*
		 * 可能是由于 heap_form_tuple 执行其操作
		 */
		fc_chunksize = VARSIZE_SHORT(fc_chunk) - VARHDRSZ_SHORT;
	}
	else
	{
		/* 永远不应该发生 */
		uint32		fc_header = ((varattrib_4b *) fc_chunk)->va_4byte.va_header;

		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u chunk %d has invalid varlena header %0x",
										 fc_ta->toast_pointer.va_valueid,
										 fc_chunk_seq, fc_header));
		return;
	}

	/*
	 * 对我们发现的数据进行一些检查
	 */
	if (fc_chunk_seq > fc_last_chunk_seq)
	{
		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u chunk %d follows last expected chunk %d",
										 fc_ta->toast_pointer.va_valueid,
										 fc_chunk_seq, fc_last_chunk_seq));
		return;
	}

	fc_expected_size = fc_chunk_seq < fc_last_chunk_seq ? TOAST_MAX_CHUNK_SIZE
		: fc_extsize - (fc_last_chunk_seq * TOAST_MAX_CHUNK_SIZE);

	if (fc_chunksize != fc_expected_size)
		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u chunk %d has size %u, but expected size %u",
										 fc_ta->toast_pointer.va_valueid,
										 fc_chunk_seq, fc_chunksize, fc_expected_size));
}

/*
 * 检查当前属性在 ctx 中的跟踪情况，记录
 * 在 ctx->tupstore 中发现的任何损坏。
 *
 * 这个函数遵循 heap_deform_tuple() 的逻辑，在
 * 刚好值的情况下，选择性地存储 toast 指针，以便稍后可以
 * 遵循 detoast_external_attr() 的逻辑进行检查，以检查
 * 任何导致这两个函数断言或崩溃后端的条件。
 * 这两个函数中存在的断言检查也在这里以及 
 * check_toasted_attribute 中执行。在这些函数对数据
 * 正确性假设稍微马虎的情况下，我们执行这些两个函数
 * 中不存在的附加检查。针对在这两个函数中检查的某些条件，
 * 我们在这里执行两次，因为我们平行这两个函数的逻辑流程。
 * 存在重复检查似乎是为了保持此代码与其保护的代码
 * 紧密耦合而付出的合理代价。
 *
 * 如果元组属性足够健全以便处理继续进行到
 * 下一个属性，则返回 true，否则返回 false。
 */
static bool fc_check_tuple_attribute(HeapCheckContext *fc_ctx)
{
	Datum		fc_attdatum;
	struct varlena *fc_attr;
	char	   *fc_tp;				/* 指向元组数据的指针 */
	uint16		fc_infomask;
	Form_pg_attribute fc_thisatt;
	struct varatt_external fc_toast_pointer;

	fc_infomask = fc_ctx->tuphdr->t_infomask;
	fc_thisatt = TupleDescAttr(RelationGetDescr(fc_ctx->rel), fc_ctx->attnum);

	fc_tp = (char *) fc_ctx->tuphdr + fc_ctx->tuphdr->t_hoff;

	if (fc_ctx->tuphdr->t_hoff + fc_ctx->offset > fc_ctx->lp_len)
	{
		fc_report_corruption(fc_ctx,
						  psprintf("attribute with length %u starts at offset %u beyond total tuple length %u",
								   fc_thisatt->attlen,
								   fc_ctx->tuphdr->t_hoff + fc_ctx->offset,
								   fc_ctx->lp_len));
		return false;
	}

	/* 跳过空值 */
	if (fc_infomask & HEAP_HASNULL && att_isnull(fc_ctx->attnum, fc_ctx->tuphdr->t_bits))
		return true;

	/* 跳过非 varlena 值，但首先更新偏移量 */
	if (fc_thisatt->attlen != -1)
	{
		fc_ctx->offset = att_align_nominal(fc_ctx->offset, fc_thisatt->attalign);
		fc_ctx->offset = att_addlength_pointer(fc_ctx->offset, fc_thisatt->attlen,
											fc_tp + fc_ctx->offset);
		if (fc_ctx->tuphdr->t_hoff + fc_ctx->offset > fc_ctx->lp_len)
		{
			fc_report_corruption(fc_ctx,
							  psprintf("attribute with length %u ends at offset %u beyond total tuple length %u",
									   fc_thisatt->attlen,
									   fc_ctx->tuphdr->t_hoff + fc_ctx->offset,
									   fc_ctx->lp_len));
			return false;
		}
		return true;
	}

	/* 好吧，我们正在查看一个 varlena 属性。 */
	fc_ctx->offset = att_align_pointer(fc_ctx->offset, fc_thisatt->attalign, -1,
									fc_tp + fc_ctx->offset);

	/* 获取（可能损坏的）varlena 数据 */
	fc_attdatum = fetchatt(fc_thisatt, fc_tp + fc_ctx->offset);

	/*
	 * 我们有这个数据，但不能不谨慎地解码它，因为它仍然可能
	 * 被损坏。
	 */

	/*
	 * 检查 VARTAG_SIZE 在损坏的 va_tag 上不会触发 TrapMacro，
	 * 然后再冒险调用 att_addlength_pointer
	 */
	if (VARATT_IS_EXTERNAL(fc_tp + fc_ctx->offset))
	{
		uint8		fc_va_tag = VARTAG_EXTERNAL(fc_tp + fc_ctx->offset);

		if (fc_va_tag != VARTAG_ONDISK)
		{
			fc_report_corruption(fc_ctx,
							  psprintf("toasted attribute has unexpected TOAST tag %u",
									   fc_va_tag));
			/* 我们无法知道下一个属性从哪里开始 */
			return false;
		}
	}

	/* 好吧，现在应该安全了 */
	fc_ctx->offset = att_addlength_pointer(fc_ctx->offset, fc_thisatt->attlen,
										fc_tp + fc_ctx->offset);

	if (fc_ctx->tuphdr->t_hoff + fc_ctx->offset > fc_ctx->lp_len)
	{
		fc_report_corruption(fc_ctx,
						  psprintf("attribute with length %u ends at offset %u beyond total tuple length %u",
								   fc_thisatt->attlen,
								   fc_ctx->tuphdr->t_hoff + fc_ctx->offset,
								   fc_ctx->lp_len));

		return false;
	}

	/*
	 * heap_deform_tuple此时会处理该属性，
	 * 并将其存储在values[]中，然后继续处理下一个属性。
	 * 我们进一步处理，因为我们需要检查toast数据是否损坏。
	 */

	fc_attr = (struct varlena *) DatumGetPointer(fc_attdatum);

	/*
	 * 现在我们遵循detoast_external_attr()的逻辑，
	 * 需要注意的是要对损坏保持警惕。
	 */

	/* 跳过非外部的值 */
	if (!VARATT_IS_EXTERNAL(fc_attr))
		return true;

	/* 它是外部的，我们正在查看磁盘上的页面 */

	/*
	 * 必须将attr复制到toast_pointer中以进行对齐考虑
	 */
	VARATT_EXTERNAL_GET_POINTER(fc_toast_pointer, fc_attr);

	/* 太大而无法解压的榨汁属性应永远不被存储 */
	if (fc_toast_pointer.va_rawsize > VARLENA_SIZE_LIMIT)
		fc_report_corruption(fc_ctx,
						  psprintf("toast value %u rawsize %d exceeds limit %d",
								   fc_toast_pointer.va_valueid,
								   fc_toast_pointer.va_rawsize,
								   VARLENA_SIZE_LIMIT));

	if (VARATT_EXTERNAL_IS_COMPRESSED(fc_toast_pointer))
	{
		ToastCompressionId fc_cmid;
		bool		fc_valid = false;

		/* 压缩属性应具有有效的压缩方法 */
		fc_cmid = TOAST_COMPRESS_METHOD(&fc_toast_pointer);
		switch (fc_cmid)
		{
				/* 所有有效压缩方法ID的列表 */
			case TOAST_PGLZ_COMPRESSION_ID:
			case TOAST_LZ4_COMPRESSION_ID:
				fc_valid = true;
				break;

				/* 被识别但无效的压缩方法ID */
			case TOAST_INVALID_COMPRESSION_ID:
				break;

				/* 故意没有默认值 */
		}
		if (!fc_valid)
			fc_report_corruption(fc_ctx,
							  psprintf("toast value %u has invalid compression method id %d",
									   fc_toast_pointer.va_valueid, fc_cmid));
	}

	/* 元组头部更好地声称包含已榨取的值 */
	if (!(fc_infomask & HEAP_HASEXTERNAL))
	{
		fc_report_corruption(fc_ctx,
						  psprintf("toast value %u is external but tuple header flag HEAP_HASEXTERNAL not set",
								   fc_toast_pointer.va_valueid));
		return true;
	}

	/* 关系更好有一个toast表 */
	if (!fc_ctx->rel->rd_rel->reltoastrelid)
	{
		fc_report_corruption(fc_ctx,
						  psprintf("toast value %u is external but relation has no toast relation",
								   fc_toast_pointer.va_valueid));
		return true;
	}

	/* 如果我们被告知跳过toast检查，则我们完成。 */
	if (fc_ctx->toast_rel == NULL)
		return true;

	/*
	 * 如果该元组有资格被修剪，我们不能检查toast。
	 * 否则，我们推送一个toast元组的副本，以便在
	 * 释放主表缓存锁后进行检查。
	 */
	if (!fc_ctx->tuple_could_be_pruned)
	{
		ToastedAttribute *fc_ta;

		fc_ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute));

		VARATT_EXTERNAL_GET_POINTER(fc_ta->toast_pointer, fc_attr);
		fc_ta->blkno = fc_ctx->blkno;
		fc_ta->offnum = fc_ctx->offnum;
		fc_ta->attnum = fc_ctx->attnum;
		fc_ctx->toasted_attributes = lappend(fc_ctx->toasted_attributes, fc_ta);
	}

	return true;
}

/*
 * 对于在ctx->toasted_attributes中收集的每个属性，查找
 * 在toast表中的值并对其进行检查。此函数应仅在
 * 我们处理期间无法被清空的toast指针上调用。
 */
static void fc_check_toasted_attribute(HeapCheckContext *fc_ctx, ToastedAttribute *fc_ta)
{
	SnapshotData fc_SnapshotToast;
	ScanKeyData fc_toastkey;
	SysScanDesc fc_toastscan;
	bool		fc_found_toasttup;
	HeapTuple	fc_toasttup;
	uint32		fc_extsize;
	int32		fc_expected_chunk_seq = 0;
	int32		fc_last_chunk_seq;

	fc_extsize = VARATT_EXTERNAL_GET_EXTSIZE(fc_ta->toast_pointer);
	fc_last_chunk_seq = (fc_extsize - 1) / TOAST_MAX_CHUNK_SIZE;

	/*
	 * 设置扫描键以查找在toast表中与va_valueid匹配的块
	 */
	ScanKeyInit(&fc_toastkey,
				(AttrNumber) 1,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(fc_ta->toast_pointer.va_valueid));

	/*
	 * 检查toast表中是否存在任何此已榨取对象的块，
	 * 通过索引访问。
	 */
	init_toast_snapshot(&fc_SnapshotToast);
	fc_toastscan = systable_beginscan_ordered(fc_ctx->toast_rel,
										   fc_ctx->valid_toast_index,
										   &fc_SnapshotToast, 1,
										   &fc_toastkey);
	fc_found_toasttup = false;
	while ((fc_toasttup =
			systable_getnext_ordered(fc_toastscan,
									 ForwardScanDirection)) != NULL)
	{
		fc_found_toasttup = true;
		fc_check_toast_tuple(fc_toasttup, fc_ctx, fc_ta, &fc_expected_chunk_seq, fc_extsize);
	}
	systable_endscan_ordered(fc_toastscan);

	if (!fc_found_toasttup)
		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u not found in toast table",
										 fc_ta->toast_pointer.va_valueid));
	else if (fc_expected_chunk_seq <= fc_last_chunk_seq)
		fc_report_toast_corruption(fc_ctx, fc_ta,
								psprintf("toast value %u was expected to end at chunk %d, but ended while expecting chunk %d",
										 fc_ta->toast_pointer.va_valueid,
										 fc_last_chunk_seq, fc_expected_chunk_seq));
}

/*
 * 检查在ctx中跟踪的当前元组，记录在
 * ctx->tupstore中发现的任何损坏。
 */
static void fc_check_tuple(HeapCheckContext *fc_ctx)
{
	/*
	 * 检查不同形式的元组头部损坏，如果头部
	 * 太过损坏，无法继续其他检查。
	 */
	if (!fc_check_tuple_header(fc_ctx))
		return;

	/*
	 * 检查元组的可见性。如果插入的事务已中止，我们
	 * 不能假设我们的关系描述与元组结构匹配，因此
	 * 不能检查它。
	 */
	if (!fc_check_tuple_visibility(fc_ctx))
		return;

	/*
	 * 元组是可见的，因此它必须与当前版本的
	 * 关系描述符兼容。它可能具有比关系描述符中
	 * 存在的列更少的列，但不能有更多。
	 */
	if (RelationGetDescr(fc_ctx->rel)->natts < fc_ctx->natts)
	{
		fc_report_corruption(fc_ctx,
						  psprintf("number of attributes %u exceeds maximum expected for table %u",
								   fc_ctx->natts,
								   RelationGetDescr(fc_ctx->rel)->natts));
		return;
	}

	/*
	 * 检查每个属性，除非我们遇到损坏，导致
	 * 不知道接下来该怎么做，此时我们将中止
	 * 针对该元组的进一步属性检查。注意我们
	 * 不会因为所有类型的损坏而中止，只会对那些
	 * 我们不知道如何继续的类型中止。我们也不会中止
	 * 在中止之前从元组收集到的已榨取属性的检查。
	 * 这些将在以后与从页面收集的其他已榨取
	 * 属性一起检查。
	 */
	fc_ctx->offset = 0;
	for (fc_ctx->attnum = 0; fc_ctx->attnum < fc_ctx->natts; fc_ctx->attnum++)
		if (!fc_check_tuple_attribute(fc_ctx))
			break;				/* 不能继续 */

	/* 将attnum恢复为-1，直到我们再次检查单个属性 */
	fc_ctx->attnum = -1;
}

/*
 * 将 TransactionId 转换为 FullTransactionId，使用我们缓存的
 * 有效事务 ID 范围的值。调用者有责任在必要时已更新缓存值。
 */
static FullTransactionId
fc_FullTransactionIdFromXidAndCtx(TransactionId fc_xid, const HeapCheckContext *fc_ctx)
{
	uint64		fc_nextfxid_i;
	int32		fc_diff;
	FullTransactionId fc_fxid;

	Assert(TransactionIdIsNormal(fc_ctx->next_xid));
	Assert(FullTransactionIdIsNormal(fc_ctx->next_fxid));
	Assert(XidFromFullTransactionId(fc_ctx->next_fxid) == fc_ctx->next_xid);

	if (!TransactionIdIsNormal(fc_xid))
		return FullTransactionIdFromEpochAndXid(0, fc_xid);

	fc_nextfxid_i = U64FromFullTransactionId(fc_ctx->next_fxid);

	/* 计算 32 位模差 */
	fc_diff = (int32) (fc_ctx->next_xid - fc_xid);

	/*
	 * 在损坏的情况下，我们可能会看到一个在纪元 0 之前的 32 位 xid。
	 * 由于 64 位 xid 是无符号整数，我们无法将其表示为 64 位 xid，
	 * 而没有 32 位 xid 的模运算。处理这一点没有很好方法，
	 * 但在这种情况下使用 FirstNormalFullTransactionId 是可以的，
	 * 因为一个新初始化的集群已经有了更新的范围。
	 */
	if (fc_diff > 0 && (fc_nextfxid_i - FirstNormalTransactionId) < (int64) fc_diff)
	{
		Assert(EpochFromFullTransactionId(fc_ctx->next_fxid) == 0);
		fc_fxid = FirstNormalFullTransactionId;
	}
	else
		fc_fxid = FullTransactionIdFromU64(fc_nextfxid_i - fc_diff);

	Assert(FullTransactionIdIsNormal(fc_fxid));
	return fc_fxid;
}

/*
 * 更新我们缓存的有效事务 ID 范围。
 */
static void fc_update_cached_xid_range(HeapCheckContext *fc_ctx)
{
	/* 制作缓存副本 */
	LWLockAcquire(XidGenLock, LW_SHARED);
	fc_ctx->next_fxid = ShmemVariableCache->nextXid;
	fc_ctx->oldest_xid = ShmemVariableCache->oldestXid;
	LWLockRelease(XidGenLock);

	/* 并计算同一事务的替代版本 */
	fc_ctx->next_xid = XidFromFullTransactionId(fc_ctx->next_fxid);
	fc_ctx->oldest_fxid = fc_FullTransactionIdFromXidAndCtx(fc_ctx->oldest_xid, fc_ctx);
}

/*
 * 更新我们缓存的有效多事务 ID 范围。
 */
static void fc_update_cached_mxid_range(HeapCheckContext *fc_ctx)
{
	ReadMultiXactIdRange(&fc_ctx->oldest_mxact, &fc_ctx->next_mxact);
}

/*
 * 返回给定的 FullTransactionId 是否在我们缓存的有效
 * 事务 ID 范围内。
 */
static inline bool fc_fxid_in_cached_range(FullTransactionId fc_fxid, const HeapCheckContext *fc_ctx)
{
	return (FullTransactionIdPrecedesOrEquals(fc_ctx->oldest_fxid, fc_fxid) &&
			FullTransactionIdPrecedes(fc_fxid, fc_ctx->next_fxid));
}

/*
 * 检查多事务 ID 是否在缓存的有效范围内，如有范围违规，
 * 则返回范围违规的性质。
 */
static XidBoundsViolation fc_check_mxid_in_range(MultiXactId fc_mxid, HeapCheckContext *fc_ctx)
{
	if (!TransactionIdIsValid(fc_mxid))
		return XID_INVALID;
	if (MultiXactIdPrecedes(fc_mxid, fc_ctx->relminmxid))
		return XID_PRECEDES_RELMIN;
	if (MultiXactIdPrecedes(fc_mxid, fc_ctx->oldest_mxact))
		return XID_PRECEDES_CLUSTERMIN;
	if (MultiXactIdPrecedesOrEquals(fc_ctx->next_mxact, fc_mxid))
		return XID_IN_FUTURE;
	return XID_BOUNDS_OK;
}

/*
 * 检查给定的 mxid 是否在正在检查的堆中有效，
 * 如有范围违规，则返回范围违规的性质。
 *
 * 此函数试图通过在 ctx 中缓存已知的有效 mxid
 * 范围快速返回。调用者应该在第一次调用此功能之前
 * 已经执行了缓存的初步设置。
 */
static XidBoundsViolation fc_check_mxid_valid_in_rel(MultiXactId fc_mxid, HeapCheckContext *fc_ctx)
{
	XidBoundsViolation fc_result;

	fc_result = fc_check_mxid_in_range(fc_mxid, fc_ctx);
	if (fc_result == XID_BOUNDS_OK)
		return XID_BOUNDS_OK;

	/* 范围可能已经推进。重新检查。 */
	fc_update_cached_mxid_range(fc_ctx);
	return fc_check_mxid_in_range(fc_mxid, fc_ctx);
}

/*
 * 检查给定的事务 ID 是否（或最近是否）有效以出现在
 * 正在检查的堆中，或是否太旧或太新而不能出现在
 * 关系中，返回有关边界违规性质的信息。
 *
 * 我们缓存有效事务 ID 的范围。如果 xid 在该范围内，
 * 我们得出结论它是有效的，即使对表的并发更改可能
 * 在某些损坏条件下使其失效。（例如，如果表
 * 包含损坏的全冻结位，则并发的清理可能会跳过
 * 包含 xid 的页面，然后截断 clog 并使 relfrozenxid
 * 超过 xid。）在这种条件下将 xid 报告为有效似乎
 * 是可以的，因为如果我们在扫描中早先检查过它，
 * 那时它确实是有效的。
 *
 * 如果状态参数不为 NULL，并且仅当事务 ID
 * 在此关系中似乎有效时，状态参数将被设置为
 * 事务 ID 的提交状态。
 */
static XidBoundsViolation fc_get_xid_status(TransactionId fc_xid, HeapCheckContext *fc_ctx,
			   XidCommitStatus *fc_status)
{
	FullTransactionId fc_fxid;
	FullTransactionId fc_clog_horizon;

	/* 对特殊 xid 的快速检查 */
	if (!TransactionIdIsValid(fc_xid))
		return XID_INVALID;
	else if (fc_xid == BootstrapTransactionId || fc_xid == FrozenTransactionId)
	{
		if (fc_status != NULL)
			*fc_status = XID_COMMITTED;
		return XID_BOUNDS_OK;
	}

	/* 检查 xid 是否在范围内 */
	fc_fxid = fc_FullTransactionIdFromXidAndCtx(fc_xid, fc_ctx);
	if (!fc_fxid_in_cached_range(fc_fxid, fc_ctx))
	{
		/*
		 * 我们可能一直在检查过时的值。更新缓存的
		 * 范围以确保，并且由于我们在执行完整的 xid
		 * 转换时依赖于缓存的范围，重新转换。
		 */
		fc_update_cached_xid_range(fc_ctx);
		fc_fxid = fc_FullTransactionIdFromXidAndCtx(fc_xid, fc_ctx);
	}

	if (FullTransactionIdPrecedesOrEquals(fc_ctx->next_fxid, fc_fxid))
		return XID_IN_FUTURE;
	if (FullTransactionIdPrecedes(fc_fxid, fc_ctx->oldest_fxid))
		return XID_PRECEDES_CLUSTERMIN;
	if (FullTransactionIdPrecedes(fc_fxid, fc_ctx->relfrozenfxid))
		return XID_PRECEDES_RELMIN;

	/* 如果调用者不请求 clog 检查，则提前返回 */
	if (fc_status == NULL)
		return XID_BOUNDS_OK;

	/* 如果我们在先前的调用中刚刚检查了这个 xid，则提前返回 */
	if (fc_xid == fc_ctx->cached_xid)
	{
		*fc_status = fc_ctx->cached_status;
		return XID_BOUNDS_OK;
	}

	*fc_status = XID_COMMITTED;
	LWLockAcquire(XactTruncationLock, LW_SHARED);
	fc_clog_horizon =
		fc_FullTransactionIdFromXidAndCtx(ShmemVariableCache->oldestClogXid,
									   fc_ctx);
	if (FullTransactionIdPrecedesOrEquals(fc_clog_horizon, fc_fxid))
	{
		if (TransactionIdIsCurrentTransactionId(fc_xid))
			*fc_status = XID_IS_CURRENT_XID;
		else if (TransactionIdIsInProgress(fc_xid))
			*fc_status = XID_IN_PROGRESS;
		else if (TransactionIdDidCommit(fc_xid))
			*fc_status = XID_COMMITTED;
		else
			*fc_status = XID_ABORTED;
	}
	LWLockRelease(XactTruncationLock);
	fc_ctx->cached_xid = fc_xid;
	fc_ctx->cached_status = *fc_status;
	return XID_BOUNDS_OK;
}
